<?php

/**
 * @file
 * Administrative interface for configuring Content Profile Form Parts.
 */

/**
 * Form builder for the Content Profile Form Parts admin form.
 *
 * @param $type
 *   The content type.
 *
 * @see profile_form_parts_admin_settings_validate()
 * @see profile_form_parts_admin_settings_submit()
 * @see profile_form_parts_block_more_submit()
 * @see profile_form_parts_block_remove_submit()
 * @see drupal_get_destination()
 *
 * @ingroup forms
 */
function profile_form_parts_admin_settings(&$form_state, $type = '') {
  // Need to cache form for AHAH, so it can be rebuilt from cache later.
  $form = array(
    '#cache' => TRUE,
  );

  if (!isset($form_state['profile_form_parts']['destination'])) {
    // Use $_GET here to retrieve the original path in source form.
    $path = isset($_GET['q']) ? $_GET['q'] : '';
    $query = drupal_query_string_encode($_GET, array('q'));
    if ($query != '') {
      $path .= '?'. $query;
    }
  }
  else {
    $path = urldecode($form_state['profile_form_parts']['destination']);
  }
  $form['destination'] = array(
    '#type' => 'hidden',
    '#value' => urlencode($path),
  );
  // Explicitly setting the form's action stops Drupal setting it to the
  // Javascript callback. This prevents a bug where the user is directed to a
  // page full of unsightly JSON data.
  $form['#action'] = url($path);

  // Container for the blocks, used for AHAH.
  $form['form_parts_blocks'] = array(
    '#tree' => TRUE,
    '#prefix' => '<div id="form-parts-blocks">',
    '#suffix' => '</div>',
    '#theme' => 'profile_form_parts_blocks',
  );

  // Should we populate the form with data from $form_state or the database?
  if (!isset($form_state['profile_form_parts']['form_parts_blocks'])) {
    $result = db_query("SELECT bid, content_type, link_text, parts FROM {profile_form_parts} WHERE content_type = '%s' ORDER BY bid", $type);
    $form_parts = array();
    while ($block = db_fetch_array($result)) {
      $block['parts'] = unserialize($block['parts']);
      $form_parts[] = $block;
    }
    $unsaved = FALSE;
  }
  else {
    // Remove unsaved warning so it is not mistaken for a block.
    unset($form_state['profile_form_parts']['form_parts_blocks']['unsaved_warning']);
    unset($form_state['profile_form_parts']['form_parts_blocks']['unsaved_warning_flag']);
    $form_parts = $form_state['profile_form_parts']['form_parts_blocks'];
    $unsaved = TRUE;
  }

  // Work through each block, adding it as a new element in
  // $form['form_parts_blocks'] ready to be themed later.
  foreach ($form_parts as $delta => $details) {
    $details['delta'] = $delta;
    $details['type'] = $type;
    $form['form_parts_blocks'][$delta] = _form_parts_block_form($details);
  }

  // The unsaved warning means the form is being built via AHAH and has changes
  // that have not been saved. This isn't shown every time there are unsaved
  // changes, but every time there is ambiguity for the user, when the 'Add
  // new block' button has been pressed for example.
  if ($unsaved) {
    $form['form_parts_blocks']['unsaved_warning'] = array(
      '#prefix' => t('There are unsaved changes.'),
      '#type' => 'submit',
      '#value' => t('Save configuration'),
    );
  }

  // Special value indicating that the unsaved message is being shown to the user
  // and therefore should continue to be shown. This form element is changed via
  // jQuery when the unsaved warning is made visible to the user.
  $form['form_parts_blocks']['unsaved_warning_flag'] = array(
    '#type' => 'hidden',
    '#default_value' => (empty($form_state['profile_form_parts']['form_parts_blocks']) || empty($form_state['values']['unsaved_warning_flag']) ? 0 : $form_state['values']['unsaved_warning_flag']),
    '#tree' => FALSE,
  );

  // The magic AHAH callback button that adds more rows.
  $form['form_parts_block_more'] = array(
    '#type' => 'submit',
    '#value' => t('More'),
    '#weight' => 1,
    '#prefix' => '<div id="add-block-button">',
    '#suffix' => '<label for="form-parts-block-more">' . t('Add new block') . '</label></div>',
    '#submit' => array('profile_form_parts_block_more_submit'),
    '#ahah' => array(
      'path' => 'profile-form-parts/blocks/js',
      'wrapper' => 'form-parts-blocks',
      'method' => 'replace',
      'effect' => 'fade',
    ),
  );

  $form['submit_form'] = array(
    '#type' => 'submit',
    '#weight' => 10,
    '#value' => t('Save configuration'),
  );

  return $form;
}

/**
 * Builds row of sending, receiving roles and actions that go with them.
 *
 * @param $details
 *   Details of the row: default values and the unique row number (delta).
 * @param $blacklist
 *   When the functionality has been added, this will allow building actions
 *   based on a whitelist or blacklist. The current code only covers the use
 *   case of a blacklist, where blocking everyone is allowed by default and
 *   rules are exceptions to that. Conversely, a whitelist will disallow
 *   blocking by default and rules will configure roles that are allowed to
 *   block.
 *
 * @return
 *   Part of a form with controls for sending, receiving and actions.
 */
function _form_parts_block_form($details) {
  $form = array(
    '#tree' => TRUE,
  );
  $delta = $details['delta'];

  // This field will be set if the block came from the database.
  if (isset($details['bid'])) {
    $form['bid'] = array(
      '#type' => 'hidden',
      '#value' => $details['bid'],
    );
  }
  $form['content_type'] = array(
    '#type' => 'hidden',
    '#value' => $details['type'],
  );
  $form['link_text'] = array(
    '#type' => 'textfield',
    '#title' => t('Link text'),
    '#attributes' => array('class' => 'link-text'),
    '#default_value' => (isset($details['link_text']) ? check_plain($details['link_text']) : ''),
    '#size' => 50,
  );

  // Get all the form parts, then loop through them creating form controls.
  $parts = _profile_form_parts($details['type']);
  $parts_data = element_children($parts);

  foreach ($parts_data as $key) {
    if (isset($parts[$key]['#type']) && in_array($parts[$key]['#type'], array('value', 'hidden', 'token')) || in_array($key, array('buttons', 'author'))) {
      continue;
    }

    $form['parts'][$key]['field'] = array(
      '#type' => 'markup',
      '#value' => (isset($parts[$key]['#title']) ? check_plain($parts[$key]['#title']) : check_plain(drupal_ucfirst(str_replace('_', ' ', $key)))),
    );
    $form['parts'][$key]['enabled'] = array(
      '#type' => 'checkbox',
      '#default_value' => (isset($details['parts'][$key]['enabled']) ? $details['parts'][$key]['enabled'] : FALSE),
    );

    $existing_weight = (isset($details['parts'][$key]['delta']) ? $details['parts'][$key]['delta'] : (isset($parts[$key]['#weight']) ? $parts[$key]['#weight'] : 0));
    // Get a reference to the greatest delta value.
    $max =& _profile_form_parts_delta_max($existing_weight);
    $form['parts'][$key]['delta'] = array(
      '#type' => 'weight',
      '#delta' => &$max,
      '#default_value' => $existing_weight,
      '#attributes' => array('class' => 'block-weight'),
    );
    $form['parts'][$key]['#attributes'] = array('class' => 'draggable');
  }
  if (!empty($form['parts'])) {
    // Sort only after values have been added to form elements, either from
    // user inputs via AHAH or defaults.
    uasort($form['parts'], '_profile_form_parts_sort');
  }
  $form['parts']['#theme'] = 'profile_form_parts_parts';
  // Unique ID for the table of form parts, used to add tabledrag functionality.
  $form['parts']['table_id'] = array(
    '#type' => 'value',
    '#value' => 'profile-form-parts-blocks-' . $delta,
  );

  $form['remove'] = array(
    '#type' => 'submit',
    '#submit' => array('profile_form_parts_block_remove_submit'),
    '#value' => 'remove-' . $delta,
    '#attributes' => array('class' => 'remove-block'),
    '#prefix' => '<div class="remove-block-button">',
    '#suffix' => '<label for="edit-remove">' . t('Remove block') . '</label></div>',
    '#ahah' => array(
      'path' => 'profile-form-parts/blocks/js',
      'wrapper' => 'form-parts-blocks',
      'method' => 'replace',
      'effect' => 'fade',
    ),
  );

  return $form;
}

/**
 * Retrieve FAPI array representing the user profile form.
 *
 * The user profile form changes according to node type and user permissions.
 * Any form parts this user cannot see, cannot be added to form blocks.
 *
 * @see drupal_get_form()
 * @see drupal_retrieve_form()
 */
function _profile_form_parts($type) {
  global $user;
  static $form;

  // Just return if $type is an empty string, we cannot get a form based
  // on that.
  if (empty($type)) {
    return;
  }

  // Only load the form once and put it into a static variable, this function
  // is designed to be called multiple times.
  if (empty($form)) {
    $node = new stdClass();
    $node->type = $type;
    // $node->name required for E_ALL compliance.
    $node->name = NULL;
    // Below liberally borrows from drupal_get_form().
    $form_id = $type . '_node_form';
    $form_state = array('storage' => NULL, 'submitted' => FALSE);
    $form = drupal_retrieve_form($form_id, $form_state, $node);
    drupal_prepare_form($form_id, $form, $form_state);
  }

  return $form;
}

/**
 * Helper function for finding the highest value delta.
 *
 * @param $delta
 *   Delta value to check.
 *
 * @return
 *   Reference to variable containing the maximum delta. This may change as
 *   form elements with greater elements are added.
 */
function &_profile_form_parts_delta_max($delta) {
  static $max = 0;

  if ($delta < 0) {
    $delta = $delta * -1;
  }

  if ($delta > $max) {
    $max = $delta;
  }

  return $max;
}

/**
 * Submit handler for 'More' button, adds a new block.
 *
 * @see profile_form_parts_admin_settings()
 * @see profile_form_parts_block_remove_submit()
 */
function profile_form_parts_block_more_submit($form, &$form_state) {
  unset($form_state['submit_handlers']);
  form_execute_handlers('submit', $form, $form_state);
  // Get the submitted actions, then put them into a special area of
  // the $form_state.
  $submitted_values = $form_state['values'];
  // Add an empty action.
  $submitted_values['form_parts_blocks'][] = array();
  $form_state['profile_form_parts'] = $submitted_values;
  // Rebuild the form by passing our $form_state through the
  // pm_block_user_settings() builder function.
  $form_state['rebuild'] = TRUE;
}

/**
 * Submit handler for 'Remove' button, removes a block.
 *
 * @see profile_form_parts_block_remove_submit()
 * @see profile_form_parts_block_more_submit()
 */
function profile_form_parts_block_remove_submit($form, &$form_state) {
  unset($form_state['submit_handlers']);
  form_execute_handlers('submit', $form, $form_state);
  $submitted_values = $form_state['values'];
  // Remove the requested block.
  $delta = $form_state['clicked_button']['#parents'][1];
  // If the block has a bid field it came from the database and should be
  // deleted from there as well as the form.
  $bid = (isset($submitted_values['form_parts_blocks'][$delta]['bid']) ? $submitted_values['form_parts_blocks'][$delta]['bid'] : NULL);
  if ($bid !== NULL) {
    db_query("DELETE FROM {profile_form_parts} WHERE bid = %d", $bid);
  }
  unset($submitted_values['form_parts_blocks'][$delta]);
  $form_state['profile_form_parts'] = $submitted_values;
  $form_state['rebuild'] = TRUE;
}

/**
 * Validation handler for the admin form.
 *
 * @see profile_form_parts_admin_settings()
 * @see profile_form_parts_admin_settings_submit()
 */
function profile_form_parts_admin_settings_validate($form, &$form_state) {
  // Only continue if it was the Save submit button that was pressed.
  if ($form_state['clicked_button']['#id'] != 'edit-submit-form') {
    return;
  }

  foreach ($form_state['values']['form_parts_blocks'] as $key => $block) {
    if (empty($block['link_text'])) {
      form_set_error('form_parts_blocks][' . $key . '][link_text', t('Link text is required.'));
    }
  }
}

/**
 * Submit handler for the admin form.
 *
 * @see profile_form_parts_admin_settings()
 * @see profile_form_parts_admin_settings_validate()
 */
function profile_form_parts_admin_settings_submit($form, &$form_state) {
  // Only save when the 'Save configuration' button has been pressed.
  if ($form_state['clicked_button']['#id'] == 'edit-submit-form') {
    unset($form_state['values']['form_parts_blocks']['unsaved_warning']);
    unset($form_state['values']['form_parts_blocks']['unsaved_warning_flag']);
    foreach ($form_state['values']['form_parts_blocks'] as $key => $block) {
      if (isset($block['bid'])) {
        drupal_write_record('profile_form_parts', $block, array('bid'));
      }
      else {
        drupal_write_record('profile_form_parts', $block);
      }
    }
    drupal_set_message(t('The configuration options have been saved.'));
    drupal_goto();
  }
}

/**
 * Menu callback for AHAH handling.
 */
function profile_form_parts_blocks_js() {
  // See: http://drupal.org/node/331941 for the philosophy of Drupal AHAH.
  $form_state = array('storage' => NULL, 'submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  $form = form_get_cache($form_build_id, $form_state);
  $args = $form['#parameters'];
  $form_id = array_shift($args);
  $form['#post'] = $_POST;
  $form['#redirect'] = FALSE;
  $form['#programmed'] = FALSE;
  $form_state['post'] = $_POST;
  drupal_process_form($form_id, $form, $form_state);
  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
  $output_form = $form['form_parts_blocks'];
  unset($output_form['#prefix'], $output_form['#suffix']); // Prevent duplicate wrappers.

  // The form renderer adds some Javascript, so the form must be rendered
  // before the Javascript.
  $output = theme('status_messages') . drupal_render($output_form);
  // Add Drupal.settings Javascript so new tables get tabledrag feature.
  $js = drupal_add_js(NULL, NULL, 'header');
  $js = drupal_get_js('header', array('setting' => $js['setting']));
  $output = $js . $output;

  drupal_json(array(
    'status' => TRUE,
    'data' => $output,
  ));
}

/**
 * Process variables for theme_profile_form_parts_blocks().
 *
 * @see profile-form-parts-blocks.tpl.php
 */
function template_preprocess_profile_form_parts_blocks(&$variables) {
  drupal_add_css(drupal_get_path('module', 'profile_form_parts') . '/profile_form_parts_admin.css');
  drupal_add_js(drupal_get_path('module', 'profile_form_parts') . '/profile_form_parts_admin.js', 'module', 'footer', FALSE, FALSE);
  if (isset($variables['form']['unsaved_warning'])) {
    $variables['unsaved_text'] = $variables['form']['unsaved_warning']['#prefix'];
    unset($variables['form']['unsaved_warning']['#prefix']);
    $variables['unsaved'] = drupal_render($variables['form']['unsaved_warning']);
    // Unset so that unsaved_warning isn't interpreted as a block in the next
    // section.
    unset($variables['form']['unsaved_warning']);
  }
  else {
    $variables['unsaved'] = FALSE;
  }

  // Find whether the hidden unsaved_warning_flag field is set to one (meaning
  // shown when the form was submitted) or zero (meaning it was not showing to
  // the user when the form was submitted).
  if ($variables['form']['unsaved_warning_flag']['#default_value'] == 1) {
    $variables['unsaved_message_shown'] = TRUE;
  }
  else {
    $variables['unsaved_message_shown'] = FALSE;
  }
  // Render the HTML for the hidden input that contains the unsaved flag.
  // This is separate to the unsaved text and 'Save configuration' button.
  $variables['unsaved_warning_flag'] = drupal_render($variables['form']['unsaved_warning_flag']);
  unset($variables['form']['unsaved_warning_flag']);

  // Get the blocks out of the form and render them.
  $form_data = element_children($variables['form']);
  $variables['block_listing'] = array();
  foreach ($form_data as $key) {
    $block = &$variables['form'][$key];
    drupal_add_tabledrag($block['parts']['table_id']['#value'], 'order', 'sibling', 'block-weight');
    $variables['block_listing'][$key] = new stdClass();
    $variables['block_listing'][$key]->link_text = drupal_render($block['link_text']);
    $variables['block_listing'][$key]->remove = drupal_render($block['remove']);
    $variables['block_listing'][$key]->parts = drupal_render($block);
  }
  // If there are no blocks drupal_add_tabledrag() will not have been called
  // and tabledrag.js will not have been added to the page.
  if (empty($form_data)) {
    drupal_add_js('misc/tabledrag.js', 'core');
  }
}

/**
 * Theme the user actions form.
 *
 * @ingroup themeable
 */
function theme_profile_form_parts_parts($parts) {
  $rows = array();
  $headers = array(
    t('Form part'),
    t('Include'),
    t('Order'),
  );
  $table_id = $parts['table_id']['#value'];
  unset($parts['table_id']);
  $form_data = element_children($parts);

  foreach ($form_data as $key) {
    // Build the table row.
    $row = array(
      'data' => array(
        array('data' => drupal_render($parts[$key]['field'])),
        array('data' => drupal_render($parts[$key]['enabled'])),
        array('data' => drupal_render($parts[$key]['delta'])),
      ),
    );

    // Add additional attributes to the row, such as a class for this row.
    if (isset($parts[$key]['#attributes'])) {
      $row = array_merge($row, $parts[$key]['#attributes']);
    }
    $rows[] = $row;
  }

  // If there are no rows, output some instructions for the user.
  if (empty($form_data)) {
    $rows[] = array(
      array(
        'data' => t("There are no form parts that can be shown on the user profile."),
        'colspan' => '3',
      ),
    );
  }

  $output = theme('table', $headers, $rows, array('id' => $table_id));
  $output .= drupal_render($parts);

  return $output;
}

/**
 * Helper function for sorting form elements.
 *
 * Sorts elements first by weight then alphabetically.
 */
function _profile_form_parts_sort($a, $b) {
  $weight = $a['delta']['#default_value'] - $b['delta']['#default_value'];
  // Order by weight.
  if ($weight) {
    return $weight;
  }

  // Order by field name.
  $a['field'] += array('#default_value' => NULL);
  $b['field'] += array('#default_value' => NULL);
  return strcmp($a['field']['#default_value'], $b['field']['#default_value']);
}
